#!/usr/bin/env python
import subprocess
import socket
import time
import dpkt
from netfilterqueue import NetfilterQueue

MIN_TTL_TO_GFW = 6
MAX_TTL_TO_GFW = 14
RANGE_OF_TTL_TO_GFW = range(MIN_TTL_TO_GFW , MAX_TTL_TO_GFW + 1)
probe_results = {}
to_gfw_ttls = {} # ip => ttl
syn_ack_ttls = {} # ip => ttl
buffered_http_requests = {} # (ip, seq) => ip_packet
scrambled_ips = []

raw_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
raw_socket.setsockopt(socket.SOL_IP, socket.IP_HDRINCL, 1)
SO_MARK = 36

def main():
    nfqueue = NetfilterQueue()
    nfqueue.bind(0, handle_packet)
    nfqueue.run()

def handle_packet(nfqueue_element):
    try:
        ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
        ip_packet.nfqueue_element = nfqueue_element
        ip_packet.src_ip = socket.inet_ntoa(ip_packet.src)
        ip_packet.dst_ip = socket.inet_ntoa(ip_packet.dst)
        if hasattr(ip_packet, 'tcp'):
            if dpkt.tcp.TH_SYN == ip_packet.tcp.flags:
                handle_syn(ip_packet)
                nfqueue_element.accept()
                return
            if (ip_packet.dst_ip, ip_packet.tcp.seq) in buffered_http_requests:
                handle_http_request(buffered_http_requests[(ip_packet.dst_ip, ip_packet.tcp.seq)])
                nfqueue_element.drop()
                return
            pos_host = ip_packet.tcp.data.find('Host:')
            if pos_host != -1:
                ip_packet.pos_host = pos_host + len('Host:')
                line_ended_at =  ip_packet.tcp.data.find('\r\n', ip_packet.pos_host)
                if line_ended_at == -1:
                    ip_packet.pos_host += 6
                else:
                    ip_packet.pos_host += ((line_ended_at - ip_packet.pos_host) / 2)
                handle_http_request(ip_packet)
                nfqueue_element.drop()
                return
        elif hasattr(ip_packet, 'icmp'):
            icmp_packet = ip_packet.data
            if dpkt.icmp.ICMP_TIMEXCEED == icmp_packet.type and dpkt.icmp.ICMP_TIMEXCEED_INTRANS == icmp_packet.code:
                handle_time_exceeded(ip_packet)
                nfqueue_element.accept()
                return
        nfqueue_element.accept()
    except:
        nfqueue_element.accept()


# === SYN: probe ttl to gfw ===
def handle_syn(ip_packet):
    if ip_packet.dst_ip in probe_results:
        probe_result = probe_results[ip_packet.dst_ip]
        if time.time() - probe_result.started_at < 10:
            return
    if ip_packet.dst_ip in to_gfw_ttls:
        return
    probe_results[ip_packet.dst_ip] = ProbeResult()
    probe_src = find_probe_src(ip_packet.dst_ip)
    inject_ping_requests_to_find_right_ttl(ip_packet.dst_ip, probe_src)

def inject_ping_requests_to_find_right_ttl(dst_ip, probe_src):
    for ttl in RANGE_OF_TTL_TO_GFW:
        icmp_packet = dpkt.icmp.ICMP(type=dpkt.icmp.ICMP_ECHO, data=dpkt.icmp.ICMP.Echo(id=ttl, seq=1, data=''))
        ip_packet = dpkt.ip.IP(
            src=socket.inet_aton(probe_src),
            dst=socket.inet_aton(dst_ip),
            p=dpkt.ip.IP_PROTO_ICMP)
        ip_packet.ttl = ttl
        ip_packet.data = icmp_packet
        raw_socket.sendto(str(ip_packet), (dst_ip, 0))


def find_probe_src(dst_ip):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect((dst_ip, 80))
        return s.getsockname()[0]
    finally:
        s.close()


class ProbeResult(object):
    def __init__(self):
        super(ProbeResult, self).__init__()
        self.started_at = time.time()
        self.routers = {} # ttl => (router_ip, is_china_router)

    def analyze_ttl_to_gfw(self, exact_match_only=True):
        for ttl in sorted(self.routers.keys()):
            next = self.routers.get(ttl + 1)
            if next is None:
                continue
                # ttl 8 is china, ttl 9 is not
            _, current_is_china_router = self.routers[ttl]
            _, next_is_china_router = next
            # then we think 8 is the ttl to gfw
            if current_is_china_router:
                if not next_is_china_router:
                    return ttl
        if exact_match_only:
            return None
        else:
            return None

# === ICMP TIME EXCEED RESPONSE: analyze probe results, find ttl to gfw ===

def handle_time_exceeded(ip_packet):
    global MAX_TTL_TO_GFW
    global MIN_TTL_TO_GFW
    global RANGE_OF_TTL_TO_GFW
    time_exceed = ip_packet.icmp.data
    if not isinstance(time_exceed.data, dpkt.ip.IP):
        return
    te_ip_packet = time_exceed.data
    dst_ip = socket.inet_ntoa(te_ip_packet.dst)
    if dst_ip in to_gfw_ttls:
        return
    if not isinstance(te_ip_packet.data, dpkt.icmp.ICMP):
        return
    te_icmp_packet = te_ip_packet.data
    if not isinstance(te_icmp_packet.data, dpkt.icmp.ICMP.Echo):
        return
    te_icmp_echo = te_icmp_packet.data
    ttl = te_icmp_echo.id
    router_ip = socket.inet_ntoa(ip_packet.src)
    is_china_router = is_china_ip(router_ip)
    probe_result = probe_results.get(dst_ip)
    if not probe_result:
        return
    probe_result.routers[ttl] = (router_ip, is_china_router)
    ttl_to_gfw = probe_result.analyze_ttl_to_gfw()
    if not ttl_to_gfw:
        return
    if MAX_TTL_TO_GFW - ttl_to_gfw > 3:
        MAX_TTL_TO_GFW = ttl_to_gfw +2
        RANGE_OF_TTL_TO_GFW = range(MIN_TTL_TO_GFW, MAX_TTL_TO_GFW + 1)
    if ttl_to_gfw - MIN_TTL_TO_GFW > 3:
        MIN_TTL_TO_GFW = ttl_to_gfw -2
        RANGE_OF_TTL_TO_GFW = range(MIN_TTL_TO_GFW, MAX_TTL_TO_GFW + 1)
    to_gfw_ttls[dst_ip] = ttl_to_gfw
    probe_results.pop(dst_ip, None)
#    if ttl_to_gfw == MAX_TTL_TO_GFW:
#        MIN_TTL_TO_GFW += 2
#        MAX_TTL_TO_GFW += 2
#        LOGGER.info('slide ttl range to [%s ~ %s]' % (MIN_TTL_TO_GFW, MAX_TTL_TO_GFW))
#        RANGE_OF_TTL_TO_GFW = range(MIN_TTL_TO_GFW, MAX_TTL_TO_GFW + 1)

# === HTTP REQUEST: buffer or scramble ===

def handle_http_request(ip_packet):
    ttl_to_gfw = to_gfw_ttls.get(ip_packet.dst_ip)
    buffer_key = (ip_packet.dst_ip, ip_packet.tcp.seq)
    if ttl_to_gfw:
        buffered_http_requests.pop(buffer_key, None)
        inject_scrambled_http_get_to_let_gfw_miss_keyword(ip_packet, ip_packet.pos_host, ttl_to_gfw)
    else:
        if buffer_key in buffered_http_requests:
            buffered_at = buffered_http_requests[buffer_key].buffered_at
            if time.time() - buffered_at > 1:
                probe_result = probe_results.pop(ip_packet.dst_ip, None)
                if probe_result:
                    ttl_to_gfw = probe_result.analyze_ttl_to_gfw(exact_match_only=False)
                else:
                    ttl_to_gfw = None
                ttl_to_gfw = ttl_to_gfw or int((MAX_TTL_TO_GFW + MIN_TTL_TO_GFW ) / 2)
                to_gfw_ttls[ip_packet.dst_ip] = ttl_to_gfw
                buffered_http_requests.pop(buffer_key, None)
                inject_scrambled_http_get_to_let_gfw_miss_keyword(ip_packet, ip_packet.pos_host, ttl_to_gfw)
        else:
            ip_packet.buffered_at = time.time()
            buffered_http_requests[buffer_key] = ip_packet


def inject_scrambled_http_get_to_let_gfw_miss_keyword(ip_packet, pos_host, ttl_to_gfw):
# we still need to make the keyword less obvious by splitting the packet into two
# to make it harder to rebuilt the stream, we injected two more fake packets to poison the stream
# first_packet .. fake_second_packet => GFW ? wrong
# fake_first_packet .. second_packet => GFW ? wrong
# first_packet .. second_packet => server ? yes, it is a HTTP GET
    global scrambled_ips
    scrambled_ips.append(ip_packet.dst_ip)
    if len(scrambled_ips) == 20:
        scrambled_ips = []
    first_part = ip_packet.tcp.data[:pos_host]
    second_part = ip_packet.tcp.data[pos_host:]

    second_packet = dpkt.ip.IP(str(ip_packet))
    second_packet.ttl = 255
    second_packet.tcp.seq += len(first_part)
    second_packet.tcp.data = second_part
    second_packet.sum = 0
    second_packet.tcp.sum = 0
    raw_socket.sendto(str(second_packet), (ip_packet.dst_ip, 0))

    fake_first_packet = dpkt.ip.IP(str(ip_packet))
    fake_first_packet.ttl = ttl_to_gfw
    fake_first_packet.tcp.data = (len(first_part) + 10) * '0'
    fake_first_packet.sum = 0
    fake_first_packet.tcp.sum = 0
    raw_socket.sendto(str(fake_first_packet), (ip_packet.dst_ip, 0))

    fake_second_packet = dpkt.ip.IP(str(ip_packet))
    fake_second_packet.ttl = ttl_to_gfw
    fake_second_packet.tcp.seq += len(first_part) + 10
    fake_second_packet.tcp.data = ': qq.com\r\n\r\n'
    fake_second_packet.sum = 0
    fake_second_packet.tcp.sum = 0
    raw_socket.sendto(str(fake_second_packet), (ip_packet.dst_ip, 0))

    first_packet = dpkt.ip.IP(str(ip_packet))
    first_packet.ttl = 255
    first_packet.tcp.data = first_part
    first_packet.sum = 0
    first_packet.tcp.sum = 0
    raw_socket.sendto(str(first_packet), (ip_packet.dst_ip, 0))

def is_china_ip(ip):
    if ip.startswith('202.97'): #CHINANET backbone
        return True
    elif ip.startswith('218.30'): #CHINANET backbone network
        return True
    elif ip.startswith('219.158'): #Backbone of China UnicomChina
        return True
    elif ip.startswith('202.112'): #China Education and Research Network Backbone
        return True
    else:
        return False        
        
if '__main__' == __name__:
    main()
